home *** CD-ROM | disk | FTP | other *** search
/ Language/OS - Multiplatform Resource Library / LANGUAGE OS.iso / smaltalk / manchest.lha / MANCHESTER / usenet / st80_pre4 / hanoi.st < prev    next >
Text File  |  1993-07-24  |  17KB  |  646 lines

  1. "    NAME        hanoi
  2.     AUTHOR        Dr Kevin Waite <kww@cs.glasgow.ac.uk>
  3.     FUNCTION Towers of Hanoi game
  4.     ST-VERSIONS    2.5
  5.     PREREQUISITES     
  6.     CONFLICTS    
  7.     DISTRIBUTION      world
  8.     VERSION        1.1
  9.     DATE        27 Nov 90
  10.     SUMMARY    Here is an implementation of the Towers-of-Hanoi game
  11.     that I think would be a useful example of the MVC is action.
  12. "!
  13. "
  14. From: kww@cs.glasgow.ac.uk (Dr Kevin Waite)
  15. Newsgroups: comp.lang.smalltalk
  16. Subject: Tower of Hanoi Game
  17. Message-ID: <7048@vanuata.cs.glasgow.ac.uk>
  18. Date: 27 Nov 90 11:38:14 GMT
  19. Organization: Computing Sci, Glasgow Univ, Scotland
  20.  
  21. Here is an implementation of the Towers-of-Hanoi game that I think would
  22. be a useful example of the MVC is action.   (BTW, did anyone outside the UK
  23. get my earlier posting of the Montana game?).   Please note that this software
  24. is purely educational - I REALLY don't spend all my time hacking games.
  25. I hope you find it useful.
  26.  
  27. Address: Dept. of Computing Science,  University of Glasgow,
  28.      17 Lilybank Gardens,  Glasgow,  United Kingdom.  G12 8QQ
  29. "
  30.  
  31.  
  32. Object subclass: #Disk
  33.     instanceVariableNames: 'size '
  34.     classVariableNames: ''
  35.     poolDictionaries: ''
  36.     category: 'Tower-of-Hanoi'!
  37. Disk comment:
  38. 'My instances represent the circular disks that are used in the Towers-of-Hanoi game.  Their only attribute is their size which is expressed as a natural number.   In a game, each size is unique.'!
  39.  
  40.  
  41. !Disk methodsFor: 'accessing'!
  42.  
  43. size
  44.     ^size!
  45.  
  46. size: aNaturalNumber
  47.     "Sets the size of the receiver to be a value in
  48.     the range [1, +oo)."
  49.  
  50.     size := aNaturalNumber max: 1.! !
  51.  
  52. !Disk methodsFor: 'testing'!
  53.  
  54. > otherDisk
  55.     "Is the receiver bigger than otherDisk?"
  56.  
  57.     ^self size > otherDisk size! !
  58. "-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- "!
  59.  
  60. Disk class
  61.     instanceVariableNames: ''!
  62.  
  63.  
  64. !Disk class methodsFor: 'instance creation'!
  65.  
  66. ofSize: aSize
  67.     ^self new size: aSize! !
  68.  
  69. MouseMenuController subclass: #TowersOfHanoiController
  70.     instanceVariableNames: ''
  71.     classVariableNames: 'HanoiMenu '
  72.     poolDictionaries: ''
  73.     category: 'Tower-of-Hanoi'!
  74. TowersOfHanoiController comment:
  75. 'My instances are used to handle user input in the TowersOfHanoi game.
  76. I provide a menu that allows the user to reset the game and have it
  77. played automatically.  My red button can be used to manually select
  78. the towers that will be the source and destination of a move
  79. operation.'!
  80.  
  81.  
  82. !TowersOfHanoiController methodsFor: 'control defaults'!
  83.  
  84. isControlActive
  85.     "I am in control so long as the cursor is within my
  86.     view's display area and the right mouse button is not
  87.     pressed."
  88.  
  89.     ^(view containsPoint: sensor cursorPoint) & sensor blueButtonPressed not! !
  90.  
  91. !TowersOfHanoiController methodsFor: 'menus'!
  92.  
  93. menu
  94.     ^HanoiMenu! !
  95.  
  96. !TowersOfHanoiController methodsFor: 'mouse activity'!
  97.  
  98. complain
  99.     "The user has made an illegal movement selection.
  100.     Flash my view to indicate my annoyance and wait
  101.     for the user to release my mouse button."
  102.  
  103.     self view flash.
  104.     Sensor waitNoButton.!
  105.  
  106. redButtonActivity
  107.     "This method allows the user to play the game
  108.     by indicating the source and destination towers
  109.     using the mouse.  Pressing down the left button
  110.     selects the source; releasing the button selects
  111.     the destination.   If the move is illegal for any 
  112.     reason then complain."
  113.  
  114.     | source dest |
  115.  
  116.     source := self towerAtCurrentPosition.
  117.     (source isNil or: [source isEmpty]) ifTrue: [^self complain].
  118.  
  119.     Cursor hand showWhile: [Sensor waitNoButton].
  120.     dest := self towerAtCurrentPosition.
  121.     (dest isNil or: [dest == source]) ifTrue: [^self complain].
  122.  
  123.     (dest willAccept: source topDisk) ifFalse: [^self complain].
  124.  
  125.     self model moveDiskFrom: source to: dest.!
  126.  
  127. towerAtCurrentPosition
  128.     "This method returns the tower currently pointed at by the
  129.     cursor.  If there is no tower at this location then return nil."
  130.  
  131.     | locus width |
  132.  
  133.     locus := Sensor cursorPoint.
  134.     (self view insetDisplayBox containsPoint: locus) ifFalse: [^nil].
  135.  
  136.     width := self view class towerWidth.
  137.     self model towers do: [:tower |
  138.         | centre xMin xMax |
  139.  
  140.         centre := self view xPositionOfTower: tower.
  141.         xMin := centre - (width // 2).
  142.         xMax := xMin + width.
  143.         ((xMin <= locus x) and: [locus x <= xMax]) ifTrue: [^tower].
  144.     ].
  145.     ^nil!
  146.  
  147. yellowButtonActivity
  148.     | action index |
  149.  
  150.     index := self menu startUpYellowButton.
  151.     action := index = 0 ifTrue: [nil] ifFalse: [self menu selectorAt: index].
  152.     action notNil ifTrue: [model perform: action].! !
  153. "-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- "!
  154.  
  155. TowersOfHanoiController class
  156.     instanceVariableNames: ''!
  157.  
  158.  
  159. !TowersOfHanoiController class methodsFor: 'initialize-release'!
  160.  
  161. initialize    
  162.     "TowersOfHanoiController initialize."
  163.  
  164.     super initialize.
  165.     HanoiMenu := ActionMenu
  166.         labels: 'Restart Game\Auto Play' withCRs
  167.         selectors: #(reset autoPlay)! !
  168.  
  169. TowersOfHanoiController initialize!
  170.  
  171.  
  172.  
  173. Object subclass: #TowersOfHanoi
  174.     instanceVariableNames: 'towers numberOfDisks moves '
  175.     classVariableNames: ''
  176.     poolDictionaries: ''
  177.     category: 'Tower-of-Hanoi'!
  178. TowersOfHanoi comment:
  179. 'My instances hold the state of play of a Tower-of-Hanoi game.   Their instance variables hold the three towers used to hold the disks (as an array) and a counter recording how many disks are being used.'!
  180.  
  181.  
  182. !TowersOfHanoi methodsFor: 'accessing'!
  183.  
  184. leftTower
  185.     ^self towers at: 1!
  186.  
  187. middleTower
  188.     ^self towers at: 2!
  189.  
  190. moves
  191.     ^moves!
  192.  
  193. numberOfDisks
  194.     ^numberOfDisks!
  195.  
  196. numberOfDisks: aNaturalNumber
  197.     "This method initializes the receiver to play
  198.     a game using the given number of disks."
  199.  
  200.     numberOfDisks := aNaturalNumber.
  201.     self reset.!
  202.  
  203. resetMoveCounter
  204.     moves := 0.!
  205.  
  206. rightTower
  207.     ^self towers at: 3!
  208.  
  209. towers
  210.     ^towers!
  211.  
  212. towers: anArrayOfTowers
  213.     towers := anArrayOfTowers.!
  214.  
  215. usedAnotherMove
  216.     moves := moves + 1.! !
  217.  
  218. !TowersOfHanoi methodsFor: 'converting'!
  219.  
  220. indexOf: aTower
  221.     "Return the index of aTower in the towers array.
  222.     This is needed by my view to position towers in
  223.     the proper part of the display.  If the index is zero
  224.     then return an error."
  225.  
  226.     | index |
  227.  
  228.     index := self towers indexOf: aTower.
  229.     index = 0 ifTrue: [self error: 'Unknown tower.'].
  230.     ^index!
  231.  
  232. optimalNumberOfMoves
  233.     "The TowersOfHanoi game can always be completed
  234.     in (2**N)-1 moves where N is the number of disks."
  235.  
  236.     ^(2 raisedToInteger: self numberOfDisks) - 1! !
  237.  
  238. !TowersOfHanoi methodsFor: 'initialize-release'!
  239.  
  240. initialize
  241.     self towers: (Array new: 3).
  242.     1 to: self towers size do: [:k |
  243.         self towers at: k put: Tower new].!
  244.  
  245. reset
  246.     self leftTower loadDisks: self numberOfDisks.
  247.     self middleTower makeEmpty.
  248.     self rightTower makeEmpty.
  249.     self resetMoveCounter.
  250.     self changed: #reset.! !
  251.  
  252. !TowersOfHanoi methodsFor: 'moving'!
  253.  
  254. moveDiskFrom: source to: destination
  255.     "This is the primitive disk moving method.  A disk
  256.     is removed from the source to the destination disk.
  257.     The method returns false if the operation fails and
  258.     true if it succeeds.   The method also requests an
  259.     update in any view that has the receiver as a model."
  260.  
  261.     | movement |
  262.  
  263.     source isEmpty ifTrue: [^false].
  264.  
  265.     (destination willAccept: source topDisk) ifFalse: [^false].
  266.  
  267.     movement := Array with: source with: destination.
  268.     self changed: #move with: movement.
  269.     destination addDisk: source removeDisk.
  270.     self usedAnotherMove.
  271.  
  272.     ^true!
  273.  
  274. moveDisks: count from: source to: dest using: temp
  275.     count > 0 ifTrue: [
  276.         self moveDisks: (count-1) from: source to: temp using: dest.
  277.         self moveDiskFrom: source to: dest.
  278.         self moveDisks: (count-1) from: temp to: dest using: source.
  279.     ].! !
  280.  
  281. !TowersOfHanoi methodsFor: 'playing'!
  282.  
  283. autoPlay
  284.     self reset.
  285.     self moveDisks: self numberOfDisks
  286.         from: self leftTower
  287.         to: self rightTower
  288.         using: self middleTower.! !
  289.  
  290. !TowersOfHanoi methodsFor: 'testing'!
  291.  
  292. finished
  293.     ^self rightTower size = self numberOfDisks! !
  294. "-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- "!
  295.  
  296. TowersOfHanoi class
  297.     instanceVariableNames: ''!
  298.  
  299.  
  300. !TowersOfHanoi class methodsFor: 'constants'!
  301.  
  302. maxNumberOfDisks
  303.     ^10! !
  304.  
  305. !TowersOfHanoi class methodsFor: 'instance creation'!
  306.  
  307. new
  308.     ^super new initialize!
  309.  
  310. open
  311.     "Create a new game and open a window onto it."
  312.  
  313.     "TowersOfHanoi open"
  314.  
  315.     | topView game myView extent |
  316.  
  317.     topView := StandardSystemView new label: 'Towers of Hanoi'.
  318.     topView borderWidth: 2.
  319.  
  320.     game := self usingDisks: 4.
  321.     myView := TowersOfHanoiView new model: game.
  322.     topView addSubView: myView.
  323.     extent := topView viewport extent.
  324.     topView minimumSize: extent.
  325.     topView maximumSize: extent.
  326.     topView controller open.!
  327.  
  328. usingDisks: count
  329.     | numberOfDisks |
  330.  
  331.     numberOfDisks := (count max: 1) min: self maxNumberOfDisks.
  332.     ^self new numberOfDisks: numberOfDisks! !
  333.  
  334. Object subclass: #Tower
  335.     instanceVariableNames: 'disks '
  336.     classVariableNames: ''
  337.     poolDictionaries: ''
  338.     category: 'Tower-of-Hanoi'!
  339. Tower comment:
  340. 'My instances represent the towers used in a Towers of Hanoi game.   Each tower can hold zero of more Disks subject to the constraint that a larger disk cannot be placed on top of a smaller one.'!
  341.  
  342.  
  343. !Tower methodsFor: 'accessing'!
  344.  
  345. disks
  346.     ^disks!
  347.  
  348. disks: aStackOfDisks
  349.     disks := aStackOfDisks.!
  350.  
  351. size
  352.     "How many disks are on the receive?"
  353.  
  354.     ^self disks size! !
  355.  
  356. !Tower methodsFor: 'disc operations'!
  357.  
  358. addDisk: aDisk
  359.     "If it is valid to do so, place aDisk on the top of the receiver
  360.     otherwise report an error."
  361.  
  362.     (self willAccept: aDisk) ifFalse: [
  363.         self error: 'Larger disks cannot be placed on smaller ones.'].
  364.  
  365.     self disks addFirst: aDisk.!
  366.  
  367. loadDisks: numberOfDisks
  368.     "Initialize the receiver with the given number of
  369.     disks.  The disks are guaranteed to be in the proper
  370.     order."
  371.  
  372.     self initialize.
  373.     numberOfDisks to: 1 by: -1 do: [:size |
  374.         self addDisk: (Disk ofSize: size)].!
  375.  
  376. makeEmpty
  377.     "This method clears out the receiver's old disks if
  378.     it has any and resets the stack of disks to be empty."
  379.  
  380.     self disks isNil ifFalse: [self release].
  381.     self initialize.!
  382.  
  383. removeDisk 
  384.     "Remove the top-most disk from the receiver and return
  385.     it as the result of the method.  If there are no disks on
  386.     the receiver then return nil."
  387.  
  388.     self isEmpty ifTrue: [^nil].
  389.     ^self disks removeFirst!
  390.  
  391. topDisk 
  392.     "Return the top-most disk of the receiver if there is
  393.     one and nil otherwise.   If the receiver is empty, return
  394.     nil.   This operation does not affect the receiver."
  395.  
  396.     self isEmpty ifTrue: [^nil].
  397.     ^self disks first! !
  398.  
  399. !Tower methodsFor: 'initialize-release'!
  400.  
  401. initialize
  402.     self disks: OrderedCollection new.!
  403.  
  404. release
  405.     self disks do: [:disk | disk release].
  406.     self disks release.
  407.     disks := nil.! !
  408.  
  409. !Tower methodsFor: 'testing'!
  410.  
  411. isEmpty
  412.     "Are there any disks on the receiver tower?"
  413.  
  414.     ^self size = 0!
  415.  
  416. willAccept: aDisk
  417.     "Is it legal to place aDisk on the receiver?  It is only legal
  418.     if the receiver is empty or its topDisk is larger than aDisk."
  419.  
  420.     ^self isEmpty or: [self topDisk > aDisk]! !
  421. "-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- "!
  422.  
  423. Tower class
  424.     instanceVariableNames: ''!
  425.  
  426.  
  427. !Tower class methodsFor: 'instance creation'!
  428.  
  429. new
  430.     ^super new initialize! !
  431.  
  432.  
  433. View subclass: #TowersOfHanoiView
  434.     instanceVariableNames: ''
  435.     classVariableNames: ''
  436.     poolDictionaries: ''
  437.     category: 'Tower-of-Hanoi'!
  438. TowersOfHanoiView comment:
  439. 'My instances provide a graphical display of the state of a TowersOfHanoi game.   When a disk is moved, its graphical representation will be moved using a simple graphical animation. '!
  440.  
  441.  
  442. !TowersOfHanoiView methodsFor: 'accessing'!
  443.  
  444. baseLine
  445.     "Return the Y ordinate of the base of the towers."
  446.  
  447.     ^self insetDisplayBox bottom - 20.!
  448.  
  449. diskScale
  450.     "The displayed width of a disk is computed from its
  451.     actual size by multiplying it by this scaling factor."
  452.  
  453.     ^self class maxDiskWidth / self model numberOfDisks!
  454.  
  455. xPositionOfLeftTower
  456.     ^self xPositionOfTower: self model leftTower!
  457.  
  458. xPositionOfMiddleTower
  459.     ^self xPositionOfTower: self model middleTower!
  460.  
  461. xPositionOfRightTower
  462.     ^self xPositionOfTower: self model rightTower!
  463.  
  464. xPositionOfTower: aTower
  465.     "This method returns the X-ordinate for the given tower.
  466.     This is expressed in display coordinates and denotes the centre
  467.     line of the tower."
  468.  
  469.     | middle offset index |
  470.  
  471.     index := self model indexOf: aTower.
  472.     middle := self insetDisplayBox center x.
  473.     index = 2 ifTrue: ["Middle" ^middle].
  474.     offset := self class towerWidth + 20.
  475.     ^index = 1 
  476.         ifTrue: ["Left" middle - offset]
  477.         ifFalse: ["Right" middle + offset]! !
  478.  
  479. !TowersOfHanoiView methodsFor: 'controller access'!
  480.  
  481. defaultControllerClass
  482.     ^TowersOfHanoiController! !
  483.  
  484. !TowersOfHanoiView methodsFor: 'displaying'!
  485.  
  486. animateFrom: source to: dest
  487.     "This method animates the movement of the disk currently 
  488.     on the top of the source tower to its new position on top
  489.     of the dest tower."
  490.  
  491.     | x1 x2 y1 y2 trajectory diskWidth image delay diskBox |
  492.  
  493.     diskWidth := source topDisk size * self diskScale.
  494.  
  495.     "The point x1@y1 is the top-left corner of the disk that is
  496.     about to be moved.   The point x2@y2 is the  position where
  497.     that disk will end up."
  498.  
  499.     x1 := (self xPositionOfTower: source) - (diskWidth // 2).
  500.     x2 := (self xPositionOfTower: dest) - (diskWidth // 2).
  501.  
  502.     y1 := self baseLine - (source size * self class diskHeight) - ((source size-1) * self class gap).
  503.     y2 := self baseLine - ((dest size+1) * self class diskHeight) - (dest size * self class gap).
  504.  
  505.     "Compute a Manhattan path between these two points.  
  506.     To animate the movement, the method will display the disk
  507.     at each point for a short period of time (the delay)."
  508.  
  509.     trajectory := self trajectoryFrom: x1@y1 to: x2@y2.
  510.     delay := Delay forMilliseconds: 50.
  511.  
  512.     "Get the image of the disk that is to be moved."
  513.     diskBox := x1@y1 extent: (diskWidth @ self class diskHeight).
  514.     image := Form fromDisplay: diskBox.
  515.  
  516.     "Erase that disk's old image on the source tower."
  517.     Display white: diskBox.
  518.  
  519.     "Animate the movement."
  520.     image follow: [trajectory next] while: [delay wait.  trajectory atEnd not].
  521.  
  522.     "Display the disk in its new position."
  523.     image displayAt: x2@y2.!
  524.  
  525. displayTower: aTower atX: xOrdinate
  526.     | y |
  527.  
  528.     y := self baseLine - self class diskHeight.
  529.     aTower disks reverseDo: [:disc |
  530.         | x box width |
  531.  
  532.         width := (self diskScale * disc size) rounded.
  533.         x := xOrdinate - (width // 2).
  534.         box := x@y extent: width@self class diskHeight.
  535.         Display black: box.
  536.         y := y - self class diskHeight - 5.
  537.     ].!
  538.  
  539. displayView
  540.     self clearInside.
  541.     self displayTower: self model leftTower atX: self xPositionOfLeftTower.
  542.     self displayTower: self model middleTower atX: self xPositionOfMiddleTower.
  543.     self displayTower: self model rightTower atX: self xPositionOfRightTower.!
  544.  
  545. insideColor
  546.     ^Form white! !
  547.  
  548. !TowersOfHanoiView methodsFor: 'window access'!
  549.  
  550. defaultWindow
  551.     | height width |
  552.  
  553.     height := self model numberOfDisks * self class diskHeight.
  554.     width := self model towers size * self class towerWidth.
  555.     width := width + (2 * self class xMargin).
  556.     height := height + (2 * self class yMargin).
  557.     ^0@0 extent: width@height! !
  558.  
  559. !TowersOfHanoiView methodsFor: 'private'!
  560.  
  561. trajectoryFrom: aPoint to: bPoint
  562.     "This method returns a ReadStream over a collection
  563.     of points.  These points describe a Manhattan path 
  564.     from aPoint to bPoint using three stages:  up, along,
  565.     down.   The number of points is controlled by the 
  566.     'intervals' class method."
  567.  
  568.     | yMin trajectory delta y x |
  569.  
  570.     yMin := self insetDisplayBox top + 10.
  571.     trajectory := OrderedCollection new: (self class intervals * 3).
  572.  
  573.     delta := (aPoint y - yMin) // self class intervals.
  574.     y := aPoint y.
  575.     self class intervals timesRepeat: [
  576.         y := y - delta.
  577.         trajectory add: (aPoint x @ y).
  578.     ].
  579.  
  580.     delta := (bPoint x - aPoint x) // self class intervals.
  581.     x := aPoint x.
  582.     self class intervals timesRepeat: [
  583.         x := x + delta.
  584.         trajectory add: (x@yMin).
  585.     ].
  586.  
  587.     delta := (bPoint y - yMin) // self class intervals.
  588.     y := yMin.
  589.     self class intervals timesRepeat: [
  590.         y := y + delta.
  591.         trajectory add: (bPoint x @ y).
  592.     ].
  593.  
  594.     ^ReadStream on: trajectory! !
  595.  
  596. !TowersOfHanoiView methodsFor: 'updating'!
  597.  
  598. update: aspect with: movement
  599.     "The receiver only recognises two changed messages.
  600.     The first is #reset which causes the entire display to be
  601.     redrawn.  The other is #move which takes an array as
  602.     its argument.  The first element in the movement array
  603.     gives the source tower, the last element the destination."
  604.  
  605.     | source destination |
  606.  
  607.     aspect == #reset ifTrue: [^self displayView].
  608.     aspect == #move ifFalse: [^self].
  609.     source := movement first.
  610.     destination := movement last.
  611.     self animateFrom: source to: destination.! !
  612. "-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- "!
  613.  
  614. TowersOfHanoiView class
  615.     instanceVariableNames: ''!
  616.  
  617.  
  618. !TowersOfHanoiView class methodsFor: 'constants'!
  619.  
  620. diskHeight
  621.     ^20!
  622.  
  623. gap
  624.     "Return the number of pixels that separate the
  625.     disks in the Y-dimension."
  626.  
  627.     ^5!
  628.  
  629. intervals
  630.     "How many steps should each animation
  631.     stage be divided into?"
  632.  
  633.     ^10!
  634.  
  635. maxDiskWidth
  636.     ^100!
  637.  
  638. towerWidth
  639.     ^120!
  640.  
  641. xMargin
  642.     ^50!
  643.  
  644. yMargin
  645.     ^50! !
  646.